Введение

Основные задачи, решаемые в страховании

  • Классические задачи:
    • тарификация
    • антифрод
    • расчет комиссии посредников
  • ML дало развитие задачам:
    • скоринг клиентов
    • оценка риска
    • оценка ущерба (резерва)
    • вероятность пролонгации
    • вероятность покупки опций
    • вероятность кросс-продаж
    • оценка параметров, неизвестных на начальном этапе (например, балл БКИ на предварительном расчете)
    • варианты развития убытка (вероятность отказа в выплате, вероятность суда, мошенничества)
    • распознавание марок-моделей ТС по фото
    • распознавание повреждений ТС по фото

Риски, оцениваемые в автостраховании

  • Повреждения кузова
    • наш клиент виновен / невиновен
    • крупные / мелкие убытки
  • Ущерб здоровью и жизни
  • Обязательная гражданская ответственность (ОСАГО)
  • Добровольная гражданская ответственность (ДАГО)
  • Повреждения в результате действия третьих лиц
  • Тоталь
  • Угон
  • Прочее

Основная идея метода бустинга

  • Строится модель, прогнозирующая целевую переменную
  • Находятся отклонения между прогнозными и фактическими значениями
  • Отклонения - новая целевая переменная
  • Строится модель, прогнозирующая отклонения
  • Находятся новые отклонения после применения двух моделей
  • Новые отклонения - снова целевая переменная
  • ...

Особенности бустинга "деревянных" моделей

  • Бόльшая склонность к переобучению, чем у линейных моделей
  • Требовательность к подбору гиперпараметров
  • Сложность с трактовкой значимости параметров

Схема решающего дерева для задачи классификации

Основная мера неоднородности при построении деревьев - энтропия Шеннона , где p - вероятность состояния, i - состояние системы. Чем выше энтропия, тем больше хаоса.

Прирост информации (изменение энтропии)

Алгоритм построения решающих деревьев

  • Вычисляем энтропию исходного множества
  • Если энтропия равна 0, значит все объекты принадлежат к одному классу
  • Разбиваем исходное множество на два подмножества, вычисляем изменение энтропии
  • Среди всех возможных разбиений выбираем то, у которого изменение энтропии наибольшее
  • Повторяем процесс: разбиваем каждое из полученных подмножеств

Для задачи регрессии

  • Мера неоднородности - это ошибка на выборке

Градиентный спуск

  • Поиск минимума ошибки за счет движения в сторону антиградиента

Что работает лучше, деревья решений или линейные модели?

  • Ответ зависит от ситуации с данными
  • В страховых задачах оценки риска преимущество "деревянных" моделей неочевидно

Решение задачи оценки ожидаемых выплат по полису КАСКО методом градиентного бустинга

Предобработка данных

In [1]:
# Подключение к Google drive

#from google.colab import drive
#drive.mount('/content/drive')
In [2]:
import numpy as np
import pandas as pd

Специфические преобразования

In [3]:
# Загрузим набор данных

#df = pd.read_csv('/content/drive/My Drive/Colab Notebooks/freMPL-R.csv', low_memory=False)
df = pd.read_csv('D:\Cloud\Git\geekbrains-ml-business\\freMPL-R.csv', low_memory=False)
df = df.loc[df.Dataset.isin([5, 6, 7, 8, 9])]
df.drop('Dataset', axis=1, inplace=True)
df.dropna(axis=1, how='all', inplace=True)
df.drop_duplicates(inplace=True)
df.reset_index(drop=True, inplace=True)

В предыдущем уроке мы заметили отрицательную величину убытка для некоторых наблюдений. Заметим, что для всех таких полисов переменная "ClaimInd" принимает только значение 0. Поэтому заменим все соответствующие значения "ClaimAmount" нулями.

In [4]:
NegClaimAmount = df.loc[df.ClaimAmount < 0, ['ClaimAmount','ClaimInd']]
print('Unique values of ClaimInd:', NegClaimAmount.ClaimInd.unique())
NegClaimAmount.head()
Unique values of ClaimInd: [0]
Out[4]:
ClaimAmount ClaimInd
82 -74.206042 0
175 -1222.585196 0
177 -316.288822 0
363 -666.758610 0
375 -1201.600604 0
In [5]:
df.loc[df.ClaimAmount < 0, 'ClaimAmount'] = 0

Для моделирования частоты убытков сгенерируем показатель как сумму индикатора того, что убыток произошел ("ClaimInd") и количества заявленных убытков по различным видам ущерба за 4 предшествующих года ("ClaimNbResp", "ClaimNbNonResp", "ClaimNbParking", "ClaimNbFireTheft", "ClaimNbWindscreen").

В случаях, если соответствующая величина убытка равняется нулю, сгенерированную частоту также обнулим.

In [6]:
df['ClaimsCount'] = df.ClaimInd + df.ClaimNbResp + df.ClaimNbNonResp + df.ClaimNbParking + df.ClaimNbFireTheft + df.ClaimNbWindscreen
df.loc[df.ClaimAmount == 0, 'ClaimsCount'] = 0
df.drop(["ClaimNbResp", "ClaimNbNonResp", "ClaimNbParking", "ClaimNbFireTheft", "ClaimNbWindscreen"], axis=1, inplace=True)
In [7]:
pd.DataFrame(df.ClaimsCount.value_counts()).rename({'ClaimsCount': 'Policies'}, axis=1)
Out[7]:
Policies
0.0 104286
2.0 3529
1.0 3339
3.0 2310
4.0 1101
5.0 428
6.0 127
7.0 26
8.0 6
9.0 2
11.0 1
In [9]:
#conda install plotly
import plotly.express as px
fig = px.scatter(df, x='ClaimsCount', y='ClaimAmount', title='Зависимость между частотой и величиной убытков')
fig.show()

Для моделирования среднего убытка можем рассчитать его как отношение величины убытков к их частоте.

In [10]:
df.loc[df.ClaimsCount > 0, 'AvgClaim'] = df['ClaimAmount']/df['ClaimsCount']

Общие преобразования

Класс для общих случаев

In [11]:
class InsDataFrame:


    ''' Load data method '''

    def load_pd(self, pd_dataframe):
        self._df = pd_dataframe


    ''' Columns match method '''

    def columns_match(self, match_from_to):
        self._df.rename(columns=match_from_to, inplace=True)


    ''' Person data methods '''

    # Gender
    _gender_dict = {'Male':0, 'Female':1}

    def transform_gender(self):
        self._df['Gender'] = self._df['Gender'].map(self._gender_dict)

        

    # Age
    @staticmethod
    def _age(age, age_max):
        if pd.isnull(age):
            age = None
        elif age < 18:
            age = None
        elif age > age_max:
            age = age_max
        return age
      
    def transform_age(self, age_max=70):
        self._df['driver_minage'] = self._df['driver_minage'].apply(self._age, args=(age_max,))

    # Age M/F
    @staticmethod
    def _age_gender(age_gender):
        _age = age_gender[0]
        _gender = age_gender[1]
        if _gender == 0: #Male
            _driver_minage_m = _age
            _driver_minage_f = 18
        elif _gender == 1: #Female
            _driver_minage_m = 18
            _driver_minage_f = _age
        else:
            _driver_minage_m = 18
            _driver_minage_f = 18
        return [_driver_minage_m, _driver_minage_f]
    
    def transform_age_gender(self):
        self._df['driver_minage_m'],self._df['driver_minage_f'] = zip(*self._df[['driver_minage','Gender']].apply(self._age_gender, axis=1).to_frame()[0])

    # Experience
    @staticmethod
    def _exp(exp, exp_max):
        if pd.isnull(exp):
            exp = None
        elif exp < 0:
            exp = None
        elif exp > exp_max:
            exp = exp_max
        return exp

    def transform_exp(self, exp_max=52):
        self._df['driver_minexp'] = self._df['driver_minexp'].apply(self._exp, args=(exp_max,))


    ''' Other data methods '''

    def polynomizer(self, column, n=2):
        if column in list(self._df.columns):
            for i in range(2,n+1):
                self._df[column+'_'+str(i)] = self._df[column]**i

    def get_dummies(self, columns):
        self._df = pd.get_dummies(self._df, columns=columns)


    ''' General methods '''

    def info(self):
        return self._df.info()

    def head(self, columns, n=5):
        return self._df.head(n)

    def len(self):
        return len(self._df)

    def get_pd(self, columns):
        return self._df[columns]

Создаем класс-наследник, в котором переопределяем некоторые методы, специфические для конкретной ситуации, и создаем новые

  • В данных стаж "LicAge" измеряется в неделях.
  • Фактор "SocioCateg" содержит информацию о социальной категории в виде кодов классификации CSP. Агрегируем имеющиеся коды до 1 знака, а затем закодируем их с помощью one-hot encoding.

Wiki

Более подробный классификатор

In [12]:
class InsDataFrame_Fr(InsDataFrame):

    # Experience (weeks to years)
    @staticmethod
    def _exp(exp, exp_max):
        if pd.isnull(exp):
            exp = None
        elif exp < 0:
            exp = None
        else:
            exp * 7 // 365
        if exp > exp_max:
            exp = exp_max
        return exp

    # Marital status
    _MariStat_dict = {'Other':0, 'Alone':1}

    def transform_MariStat(self):
        self._df['MariStat'] = self._df['MariStat'].map(self._MariStat_dict)
    
    # Social category
    def transform_SocioCateg(self):
        self._df['SocioCateg'] = self._df['SocioCateg'].str.slice(0,4)

Подгружаем данные

In [13]:
data = InsDataFrame_Fr()
In [14]:
data.load_pd(df)
In [15]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 17 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   Exposure     115155 non-null  float64
 1   LicAge       115155 non-null  int64  
 2   RecordBeg    115155 non-null  object 
 3   RecordEnd    59455 non-null   object 
 4   Gender       115155 non-null  object 
 5   MariStat     115155 non-null  object 
 6   SocioCateg   115155 non-null  object 
 7   VehUsage     115155 non-null  object 
 8   DrivAge      115155 non-null  int64  
 9   HasKmLimit   115155 non-null  int64  
 10  BonusMalus   115155 non-null  int64  
 11  ClaimAmount  115155 non-null  float64
 12  ClaimInd     115155 non-null  int64  
 13  OutUseNb     115155 non-null  float64
 14  RiskArea     115155 non-null  float64
 15  ClaimsCount  115155 non-null  float64
 16  AvgClaim     10869 non-null   float64
dtypes: float64(6), int64(5), object(6)
memory usage: 14.9+ MB

Преобразовываем параметры

In [16]:
# Переименовываем
data.columns_match({'DrivAge':'driver_minage','LicAge':'driver_minexp'})
In [17]:
# Преобразовываем
data.transform_age()
data.transform_exp()
data.transform_gender()
data.transform_MariStat()
data.transform_SocioCateg()
In [18]:
# Пересечение пола и возраста, их квадраты
data.transform_age_gender()
data.polynomizer('driver_minage_m')
data.polynomizer('driver_minage_f')

Для переменных, содержащих более 2 значений:

  • либо упорядочиваем значения,
  • либо используем фиктивные переменные (one-hot encoding).
In [19]:
# Onehot encoding
data.get_dummies(['VehUsage','SocioCateg'])
In [20]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 30 columns):
 #   Column                           Non-Null Count   Dtype  
---  ------                           --------------   -----  
 0   Exposure                         115155 non-null  float64
 1   driver_minexp                    115155 non-null  int64  
 2   RecordBeg                        115155 non-null  object 
 3   RecordEnd                        59455 non-null   object 
 4   Gender                           115155 non-null  int64  
 5   MariStat                         115155 non-null  int64  
 6   driver_minage                    115155 non-null  int64  
 7   HasKmLimit                       115155 non-null  int64  
 8   BonusMalus                       115155 non-null  int64  
 9   ClaimAmount                      115155 non-null  float64
 10  ClaimInd                         115155 non-null  int64  
 11  OutUseNb                         115155 non-null  float64
 12  RiskArea                         115155 non-null  float64
 13  ClaimsCount                      115155 non-null  float64
 14  AvgClaim                         10869 non-null   float64
 15  driver_minage_m                  115155 non-null  int64  
 16  driver_minage_f                  115155 non-null  int64  
 17  driver_minage_m_2                115155 non-null  int64  
 18  driver_minage_f_2                115155 non-null  int64  
 19  VehUsage_Private                 115155 non-null  uint8  
 20  VehUsage_Private+trip to office  115155 non-null  uint8  
 21  VehUsage_Professional            115155 non-null  uint8  
 22  VehUsage_Professional run        115155 non-null  uint8  
 23  SocioCateg_CSP1                  115155 non-null  uint8  
 24  SocioCateg_CSP2                  115155 non-null  uint8  
 25  SocioCateg_CSP3                  115155 non-null  uint8  
 26  SocioCateg_CSP4                  115155 non-null  uint8  
 27  SocioCateg_CSP5                  115155 non-null  uint8  
 28  SocioCateg_CSP6                  115155 non-null  uint8  
 29  SocioCateg_CSP7                  115155 non-null  uint8  
dtypes: float64(6), int64(11), object(2), uint8(11)
memory usage: 17.9+ MB
In [21]:
col_features = [
                'driver_minexp',
                'Gender',
                'MariStat',
                'HasKmLimit',
                'BonusMalus',
                'OutUseNb',
                'RiskArea',
                'driver_minage_m',
                'driver_minage_f',
                'driver_minage_m_2',
                'driver_minage_f_2',
                'VehUsage_Private',
                'VehUsage_Private+trip to office',
                'VehUsage_Professional',
                'VehUsage_Professional run',
                'SocioCateg_CSP1',
                'SocioCateg_CSP2',
                'SocioCateg_CSP3',
                'SocioCateg_CSP4',
                'SocioCateg_CSP5',
                'SocioCateg_CSP6',
                'SocioCateg_CSP7'  
]
In [22]:
col_target = ['ClaimAmount', 'ClaimsCount', 'AvgClaim']
In [23]:
df_freq = data.get_pd(col_features+col_target)
In [24]:
df_ac = df_freq[df_freq['ClaimsCount'] > 0].reset_index().copy()

Разделение набора данных на обучающую, валидационную и тестовую выборки

In [25]:
from sklearn.model_selection import train_test_split
In [26]:
# Разбиение датасета для частоты на train/val/test

x_train_c, x_test_c, y_train_c, y_test_c = train_test_split(df_freq[col_features], df_freq.ClaimsCount, test_size=0.3, random_state=1)
x_valid_c, x_test_c, y_valid_c, y_test_c = train_test_split(x_test_c, y_test_c, test_size=0.5, random_state=1)
In [27]:
# Разбиение датасета для среднего убытка на train/val/test 

x_train_ac, x_test_ac, y_train_ac, y_test_ac = train_test_split(df_ac[col_features], df_ac.AvgClaim, test_size=0.3, random_state=1)
x_valid_ac, x_test_ac, y_valid_ac, y_test_ac = train_test_split(x_test_ac, y_test_ac, test_size=0.5, random_state=1)

Градиентный бустинг

Градиентный бустинг - ансамблевый метод машинного обучения, использующийся для задач классификации, регрессии и ранжирования. Ансамбль представляет собой композицию простых базовых алгоритмов, в качестве которых обычно используются деревья решений.

Классический алгоритм GBM был предложен Джеромом Фридманом в 1999 году. Популярность методов GBM пришла в 2015-2016 гг. благодаря большому успеху библиотеки XGBoost в соревнованиях Kaggle.

Популярные библиотеки для GBM:

  • XGBoost (eXtreme Gradient Boosting)
    • скорость, масштабируемость, поддержка распределенных вычислений
    • добавление компоненты регуляризации, отсутствовавшей в классическом алгоритме GBM
  • LightGBM
    • использование алгоритмов, основанных на гистограммах, которые позволяют сократить время исполнения и потребление памяти в процессе обучения моделей.
  • CatBoost (Categorical Boosting)
    • предназначается для эффективной работы с категориальными признаками

Все перечисленные библиотеки зачастую имеют сравнимый результат.

Теория (XGBoost)

Модель

Пусть $y_i$ – значение переменной, которое необходимо предсказать, $x_i$ – входные данные.

Модель имеет вид $$\hat{y}_i = \sum_{k=1}^K f_k(x_i),\hspace{10pt} f_k \in \mathcal{F},$$ где $K$ – количество деревьев, $f$ – функция на пространстве $\mathcal{F}$, которое содержит все возможные деревья решений.

Целевая функция $$\text{Obj}(\theta) = L(\theta) + \Omega(\theta),$$ где

  • $\theta$ – параметры модели;
  • $L$ – величина потерь на обучающей выборке (насколько хорошо модель описывает данные?);
  • $\Omega$ – компонента, отвечающая за регуляризацию (насколько модель сложная?).

Тогда для представленной модели $\theta = \{f_1,f_2,\cdots,f_K\}$,

$$\text{Obj} = \sum_{i=1}^n l(y_i, \hat{y}_i) + \sum_{k=1}^K\Omega(f_k),$$

где $l(y_i, \hat{y}_i)$ – функция потерь.

Обучение

Необходимо обучить функции $f_i$, каждая из которых включает структуру дерева и значения листьев.

Обозначим $\hat{y}_i^{(t)}$ предсказанное значение на шаге $t$. Тогда целевая функция имеет вид: $$\text{Obj} = \sum_{i=1}^n l\left(y_i, \hat{y}_i^{(t)}\right) + \sum_{i=1}^t\Omega(f_i).$$

Обучение деревьев происходит поочередно, начиная с постоянного предсказания: $$\begin{split}\hat{y}_i^{(0)} &= 0\\ \hat{y}_i^{(1)} &= f_1(x_i) = \hat{y}_i^{(0)} + f_1(x_i)\\ \hat{y}_i^{(2)} &= f_1(x_i) + f_2(x_i)= \hat{y}_i^{(1)} + f_2(x_i)\\ &\dots\\ \hat{y}_i^{(t)} &= \sum_{k=1}^t f_k(x_i)= \hat{y}_i^{(t-1)} + f_t(x_i)\end{split}$$

На каждом шаге выбирается дерево, которое оптимизирует целевую функцию. $$\begin{split}\text{Obj}^{(t)} & = \sum_{i=1}^n l\left(y_i, \hat{y}_i^{(t)}\right) + \sum_{i=1}^t\Omega(f_i) \\ & = \sum_{i=1}^n l\left(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)\right) + \Omega(f_t) + \mathrm{constant}\end{split}$$

Для упрощения задачи оптимизации для заданной функции потерь используется разложение Тейлора: $$F(x+\Delta x) \simeq F(x) + F'(x)\Delta x + \frac{1}{2} F''(x)\Delta x^2 + \dots$$

Тогда обозначив градиент и гессиан функции потерь соответственно $$g_i = \partial_{\hat{y}_i^{(t-1)}} l(y_i, \hat{y}_i^{(t-1)}),\hspace{10pt}h_i = \partial_{\hat{y}_i^{(t-1)}}^2 l(y_i, \hat{y}_i^{(t-1)}),$$ целевая функция будет иметь вид $$\text{Obj}^{(t)} = \sum_{i=1}^n [l(y_i, \hat{y}_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t) + \mathrm{constant}.$$

Убирая константы на шаге $t$, целевая функция упрощается в виде: $$\text{Obj}^{(t)} = \sum_{i=1}^n [g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i)] + \Omega(f_t).$$

Таким образом, величина потерь $L$ зависит только от $g_i$ и $h_i$.

Благодаря этому, XGBoost поддерживает пользовательские целевые функции, для которых достаточно задать градиент и гессиан функции потерь.

Регуляризация

Для начала определим дерево $f_t(x)$: $$f_t(x) = w_{q(x)}, w \in R^T, q:R^d\rightarrow \{1,2,\cdots,T\},$$ где $w$ – вектор значений на листьях дерева, $T$ – количество листьев, $q$ – функция, которая каждой точке набора данных ставит в соответствие лист дерева.

Тогда сложность модели имеет вид $$\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^T w_j^2+ \alpha \sum_{j=1}^T |w_j|,$$ где $\gamma$ – штраф на сложность деревьев, $\lambda$ – сила регуляризации $\ell_2$, $\alpha$ – сила регуляризации $\ell_1$.

Переобучение Для контроля переобучения помимо параметров $\gamma$, $\alpha$ и $\lambda$ используются также параметры:

  • Контролирующие сложность модели напрямую
    • максимальная глубина дерева (max_depth)
    • минимальный вес в узле, ниже которого прекращается дальнейшее разделение в этом узле (min_child_weight)
  • Добавляющие случайность, повышая устойчивость к зашумлению
    • $\eta$ – величина шага. $\eta \in (0,1]$. Вместо $\hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + f_t(x_i)$ используется $\hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + \eta\cdot f_t(x_i)$
    • Доля подвыборки наблюдений для построения деревьев (subsample)
    • Доля подвыборки признаков для построения деревьев (colsample_bytree)

Теория, стоящая за оценкой весов на листьях и нахождением разделений выходит за рамки нашего рассмотрения.

Выбор гиперпараметров

Основные гиперпараметры, используемые в библиотеке XGBoost

  • objective - функция распределения
  • eta - размер шага
  • _maxdepth - максимальная глубина дерева
  • _min_childweight - минимальный вес, необходимый дочерним элементам
  • subsample - доля подвыборки для каждой итерации
  • _colsamplebytree - доля колонок, участвующих в итерации
  • alpha - сила регуляризации L1
  • lambda - сила регуляризации L2
  • gamma - штраф на сложность деревьев
  • _num_boostround - число итераций (фиксируем, не следует менять вместе с eta)
  • _early_stoppingrounds - число итераций для остановки, если не произошло улучшение метрики (фиксируем)

Стратегии подбора гиперпараметров:

  • Поиск на сетке (Grid Search, Randomized Search)
  • Покоординатный спуск (Coordinate Descent)
  • Генетические алгоритмы (Genetic Algorithms)
  • Байесовская оптимизация
  • ...

Использование некоторых алгоритмов подбора гиперпараметров для XGBoost

Для подбора параметров воспользуемся реализацией алгоритма Tree-structured Parzen Estimator (TPE) в библиотеке Hyperopt. Алгоритм использует подход последовательной оптимизации, основанной на модели (sequential model-based optimization, SMBO). Метод основывается в байесовской оптимизации и гауссовских процессах.

In [ ]:
#!pip install hyperopt --upgrade
In [30]:
from functools import partial
#Anaconda Prompt command:
#conda install xgboost -- does not found
#pip install xgboost -- success
import xgboost as xgb
from hyperopt import hp, fmin, tpe, space_eval, Trials, STATUS_OK

Построение модели градиентного бустинга для частоты страховых случаев

In [31]:
# Конвертация наборов данных в формат, поддерживающийся XGBoost

train_c = xgb.DMatrix(x_train_c, y_train_c)
valid_c = xgb.DMatrix(x_valid_c, y_valid_c)
test_c = xgb.DMatrix(x_test_c, y_test_c)
In [32]:
# Зададим функцию Deviance для распределения Пуассона

def xgb_eval_dev_poisson(yhat, y):
    t_hat, t = yhat + 1, y.get_label() + 1
    return 'dev_poisson', 2 * np.sum(t * np.log(t / t_hat) - (t - t_hat))
In [33]:
# Определим функцию для оптимизации гиперпараметров алгоритмом TPE

def objective(params, cv_params, data):
    if 'max_depth' in params.keys():
        params['max_depth'] = int(params['max_depth'])
    cv_result = xgb.cv(params=params, dtrain=data, **cv_params)
    name = [i for i in cv_result.columns if all([i.startswith('test-'), i.endswith('-mean')])][-1]
    score = cv_result[name][-1:].values[0]
    return {'loss': score, 'status': STATUS_OK}
In [34]:
# Определим параметры выполнения кроссвалидации

cv_params = {'num_boost_round': 300,
             'nfold': 5,
             'shuffle': True,
             'stratified': False,
             'feval': xgb_eval_dev_poisson,
             'maximize': False,
             'early_stopping_rounds': 20
              }
In [35]:
# Определим границы, в которых будем искать гиперпараметры

space_freq = {'objective': 'count:poisson',
              'max_depth': hp.choice('max_depth', [5, 8, 10, 12, 15]),
              'min_child_weight': hp.uniform('min_child_weight', 0, 50),
              'subsample': hp.uniform('subsample', 0.5, 1),
              'colsample_bytree': hp.uniform('colsample_bytree', 0.5, 1),
              'alpha': hp.uniform('alpha', 0, 1),
              'lambda': hp.uniform('lambda', 0, 1),
              'eta': hp.uniform('eta', 0.01, 1),
              'tree_method': 'hist'
              }
In [36]:
# Оптимизация (количество итераций снижено для ускорения работы)

trials = Trials()
best = fmin(fn=partial(objective, cv_params=cv_params, data=train_c),
            space=space_freq, trials=trials, algo=tpe.suggest, max_evals=50, timeout=3600)
100%|██████████████████████████████████████████████████████| 50/50 [16:13<00:00, 19.46s/trial, best loss: 4863.1033202]
In [41]:
# Оптимальные гиперпараметры 

best_params = space_eval(space_freq, best)
best_params
Out[41]:
{'alpha': 0.7823793679630652,
 'colsample_bytree': 0.5756926319007278,
 'eta': 0.3265133741995335,
 'lambda': 0.7178150096082871,
 'max_depth': 5,
 'min_child_weight': 48.95546449714675,
 'objective': 'count:poisson',
 'subsample': 0.6577116024105144,
 'tree_method': 'hist'}
In [42]:
train_params = {'num_boost_round': 300,
                'feval': xgb_eval_dev_poisson,
                'maximize': False,
                'verbose_eval': False}
In [43]:
# Построение модели с ранней остановкой (early stopping)

progress = dict()
xgb_freq = xgb.train(params=best_params, dtrain=train_c, early_stopping_rounds=10,
                     evals=[(train_c, "train"), (valid_c, "valid")],
                     evals_result=progress, **train_params)
In [44]:
# Построение модели без ранней остановки

progress_wo_es = dict()
xgb_freq_wo_es = xgb.train(params=best_params, dtrain=train_c, evals=[(train_c, "train"), (valid_c, "valid"), (test_c, "test")],
                           evals_result=progress_wo_es, **train_params)
In [45]:
import matplotlib.pyplot as plt
plt.subplots(2,2, figsize=(10,6))
plt.subplot(2,2,1)
plt.plot(progress_wo_es['valid']['poisson-nloglik'], label='valid')
plt.plot(progress['valid']['poisson-nloglik'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['poisson-nloglik'], label='test')
plt.xlabel('Epochs'); plt.ylabel('Negative Log-Likelihood'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,2)
plt.plot(progress_wo_es['valid']['dev_poisson'], label='valid')
plt.plot(progress['valid']['dev_poisson'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['dev_poisson'], label='test')
plt.xlabel('Epochs'); plt.ylabel('Poisson Deviance'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,3)
plt.plot(progress_wo_es['valid']['poisson-nloglik'], label='valid')
plt.plot(progress['valid']['poisson-nloglik'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['poisson-nloglik'], label='test')
plt.plot(progress_wo_es['train']['poisson-nloglik'], label='train')
plt.xlabel('Epochs'); plt.ylabel('Negative Log-Likelihood'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,4)
plt.plot(progress_wo_es['valid']['dev_poisson'], label='valid')
plt.plot(progress['valid']['dev_poisson'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['dev_poisson'], label='test')
plt.plot(progress_wo_es['train']['dev_poisson'], label='train')
plt.xlabel('Epochs'); plt.ylabel('Poisson Deviance'); plt.legend(); plt.tight_layout(); plt.show()
In [46]:
# Отбор признаков (Feature Importance)

importance_type = ['total_gain', 'gain', 'weight', 'total_cover', 'cover']
xgb.plot_importance(xgb_freq, importance_type=importance_type[1]); plt.show()

Построение модели градиентного бустинга для среднего убытка

In [47]:
# Конвертация наборов данных в формат, поддерживающийся XGBoost

train_ac = xgb.DMatrix(x_train_ac, y_train_ac)
valid_ac = xgb.DMatrix(x_valid_ac, y_valid_ac)
test_ac = xgb.DMatrix(x_test_ac, y_test_ac)
In [48]:
# Зададим функцию Deviance для гамма-распределения

def xgb_eval_dev_gamma(yhat, dtrain):
    y = dtrain.get_label()
    return 'dev_gamma', 2 * np.sum(-np.log(y/yhat) + (y-yhat)/yhat)
In [49]:
# Определим границы, в которых будем искать гиперпараметры 

space_avgclm = {'objective': 'reg:gamma',
                'max_depth': hp.choice('max_depth', [5, 8, 10, 12, 15]),
                'min_child_weight': hp.uniform('min_child_weight', 0, 50),
                'subsample': hp.uniform('subsample', 0.5, 1),
                'colsample_bytree': hp.uniform('colsample_bytree', 0.5, 1),
                'alpha': hp.uniform('alpha', 0, 1),
                'lambda': hp.uniform('lambda', 0, 1),
                'eta': hp.uniform('eta', 0.01, 1),
                'tree_method': 'hist'
                }        
In [50]:
# Определим параметры выполнения кроссвалидации

cv_params = {'num_boost_round': 300,
             'nfold': 5,
             'shuffle': True,
             'stratified': False,
             'feval': xgb_eval_dev_gamma,
             'maximize': False,
             'early_stopping_rounds': 20
              }
In [51]:
# Оптимизация (количество итераций снижено для ускорения работы)

trials = Trials()
best = fmin(fn=partial(objective, cv_params=cv_params, data=train_ac),
            space=space_avgclm, trials=trials, algo=tpe.suggest, max_evals=50, timeout=3600)
100%|████████████████████████████████████████████████| 50/50 [02:27<00:00,  2.94s/trial, best loss: 3215.0859864000004]
In [52]:
# Оптимальные гиперпараметры 

best_params = space_eval(space_avgclm, best)
best_params
Out[52]:
{'alpha': 0.9714097935020924,
 'colsample_bytree': 0.5009151891707834,
 'eta': 0.5742606058978148,
 'lambda': 0.5261341969762154,
 'max_depth': 5,
 'min_child_weight': 45.582494420769464,
 'objective': 'reg:gamma',
 'subsample': 0.868659511416018,
 'tree_method': 'hist'}
In [53]:
train_params = {'num_boost_round': 300,
                'feval': xgb_eval_dev_gamma,
                'maximize': False,
                'verbose_eval': False}
In [54]:
# Построение модели с ранней остановкой (early stopping)

progress = dict()
xgb_avgclaim = xgb.train(params=best_params, dtrain=train_ac, early_stopping_rounds=10, evals=[(train_ac, "train"), (valid_ac, "valid")],
                         evals_result=progress, **train_params)
In [55]:
# Построение модели без ранней остановки

progress_wo_es = dict()
xgb_avgclaim_wo_es = xgb.train(params=best_params, dtrain=train_ac, evals=[(train_ac, "train"), (valid_ac, "valid"), (test_ac, "test")],
                               evals_result=progress_wo_es, **train_params)
In [56]:
plt.subplots(2,2, figsize=(10,6))
plt.subplot(2,2,1)
plt.plot(progress_wo_es['valid']['gamma-nloglik'], label='valid')
plt.plot(progress['valid']['gamma-nloglik'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['gamma-nloglik'], label='test')
plt.xlabel('Epochs'); plt.ylabel('Negative Log-Likelihood'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,2)
plt.plot(progress_wo_es['valid']['dev_gamma'], label='valid')
plt.plot(progress['valid']['dev_gamma'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['dev_gamma'], label='test')
plt.xlabel('Epochs'); plt.ylabel('Gamma Deviance'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,3)
plt.plot(progress_wo_es['valid']['gamma-nloglik'], label='valid')
plt.plot(progress['valid']['gamma-nloglik'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['gamma-nloglik'], label='test')
plt.plot(progress_wo_es['train']['gamma-nloglik'], label='train')
plt.xlabel('Epochs'); plt.ylabel('Negative Log-Likelihood'); plt.legend(); plt.tight_layout()
plt.subplot(2,2,4)
plt.plot(progress_wo_es['valid']['dev_gamma'], label='valid')
plt.plot(progress['valid']['dev_gamma'], label='valid early stopping', linestyle='dashed', color='black')
plt.plot(progress_wo_es['test']['dev_gamma'], label='test')
plt.plot(progress_wo_es['train']['dev_gamma'], label='train')
plt.xlabel('Epochs'); plt.ylabel('Gamma Deviance'); plt.legend(); plt.tight_layout(); plt.show()
In [57]:
# Отбор признаков (Feature Importance)

importance_type = ['total_gain', 'gain', 'weight', 'total_cover', 'cover']
xgb.plot_importance(xgb_avgclaim, importance_type=importance_type[1]); plt.show()

Интерпретация моделей градиентного бустинга

In [58]:
#!pip install eli5
In [60]:
import eli5
C:\Users\mmingalov\Anaconda3\lib\site-packages\sklearn\utils\deprecation.py:143: FutureWarning:

The sklearn.metrics.scorer module is  deprecated in version 0.22 and will be removed in version 0.24. The corresponding classes / functions should instead be imported from sklearn.metrics. Anything that cannot be imported from sklearn.metrics is now part of the private API.

C:\Users\mmingalov\Anaconda3\lib\site-packages\sklearn\utils\deprecation.py:143: FutureWarning:

The sklearn.feature_selection.base module is  deprecated in version 0.22 and will be removed in version 0.24. The corresponding classes / functions should instead be imported from sklearn.feature_selection. Anything that cannot be imported from sklearn.feature_selection is now part of the private API.

In [61]:
eli5.show_weights(xgb_avgclaim)
Out[61]:
Weight Feature
0.1341 MariStat
0.0872 SocioCateg_CSP2
0.0738 Gender
0.0614 SocioCateg_CSP4
0.0610 RiskArea
0.0549 driver_minage_m
0.0545 driver_minage_f
0.0545 VehUsage_Private+trip to office
0.0478 BonusMalus
0.0465 SocioCateg_CSP5
0.0411 OutUseNb
0.0385 driver_minage_m_2
0.0359 HasKmLimit
0.0356 VehUsage_Private
0.0354 VehUsage_Professional run
0.0333 driver_minexp
0.0312 SocioCateg_CSP6
0.0294 SocioCateg_CSP1
0.0261 VehUsage_Professional
0.0179 driver_minage_f_2
… 2 more …
In [62]:
eli5.explain_prediction(xgb_avgclaim, x_test_ac.iloc[0,:])
Out[62]:

y (score 7.583) top features

Contribution? Feature
+7.809 <BIAS>
+0.045 driver_minage_m
+0.041 driver_minage_f_2
+0.023 VehUsage_Private
+0.007 SocioCateg_CSP4
+0.005 VehUsage_Professional run
+0.004 SocioCateg_CSP2
+0.002 OutUseNb
+0.002 SocioCateg_CSP1
-0.005 driver_minage_m_2
-0.009 BonusMalus
-0.011 driver_minexp
-0.012 VehUsage_Professional
-0.012 SocioCateg_CSP6
-0.034 SocioCateg_CSP5
-0.045 MariStat
-0.096 driver_minage_f
-0.129 RiskArea
In [63]:
np.log(xgb_avgclaim.predict(xgb.DMatrix(x_test_ac.iloc[[0],:])))
Out[63]:
array([6.8896117], dtype=float32)
In [ ]:
#!pip install shap
In [66]:
#Anaconda Prompt:
#conda install shap -- does not install
#pip install shap -- does not install
#conda install -c conda-forge shap --success!

    #conda-forge is a community effort that provides conda packages for a wide range of software. 
    #Missing a package that you would love to install with conda? - Chances are we have already packaged it for you! 

#pip install --upgrade setuptools

import shap
In [70]:
explainer = shap.TreeExplainer(xgb_avgclaim)
shap_values = explainer.shap_values(x_test_ac)
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-70-9534465fd8d4> in <module>
----> 1 explainer = shap.TreeExplainer(xgb_avgclaim)
      2 shap_values = explainer.shap_values(x_test_ac)

~\Anaconda3\lib\site-packages\shap\explainers\tree.py in __init__(self, model, data, model_output, feature_perturbation, **deprecated_options)
    119         self.feature_perturbation = feature_perturbation
    120         self.expected_value = None
--> 121         self.model = TreeEnsemble(model, self.data, self.data_missing, model_output)
    122         self.model_output = model_output
    123         #self.model_output = self.model.model_output # this allows the TreeEnsemble to translate model outputs types by how it loads the model

~\Anaconda3\lib\site-packages\shap\explainers\tree.py in __init__(self, model, data, data_missing, model_output)
    724             self.original_model = model
    725             self.model_type = "xgboost"
--> 726             xgb_loader = XGBTreeModelLoader(self.original_model)
    727             self.trees = xgb_loader.get_trees(data=data, data_missing=data_missing)
    728             self.base_offset = xgb_loader.base_score

~\Anaconda3\lib\site-packages\shap\explainers\tree.py in __init__(self, xgb_model)
   1324         self.read_arr('i', 29) # reserved
   1325         self.name_obj_len = self.read('Q')
-> 1326         self.name_obj = self.read_str(self.name_obj_len)
   1327         self.name_gbm_len = self.read('Q')
   1328         self.name_gbm = self.read_str(self.name_gbm_len)

~\Anaconda3\lib\site-packages\shap\explainers\tree.py in read_str(self, size)
   1454 
   1455     def read_str(self, size):
-> 1456         val = self.buf[self.pos:self.pos+size].decode('utf-8')
   1457         self.pos += size
   1458         return val

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 335: invalid start byte
In [71]:
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values[0,:], x_test_ac.iloc[0,:])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-71-1575e19ae8c8> in <module>
      1 shap.initjs()
----> 2 shap.force_plot(explainer.expected_value, shap_values[0,:], x_test_ac.iloc[0,:])

NameError: name 'explainer' is not defined
In [72]:
shap.waterfall_plot(explainer.expected_value, shap_values[0,:], x_test_ac.iloc[0,:])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-72-82f6e9bea429> in <module>
----> 1 shap.waterfall_plot(explainer.expected_value, shap_values[0,:], x_test_ac.iloc[0,:])

NameError: name 'explainer' is not defined
In [73]:
shap.initjs()
shap.force_plot(explainer.expected_value, shap_values, x_test_ac)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-73-96cae3a04e1b> in <module>
      1 shap.initjs()
----> 2 shap.force_plot(explainer.expected_value, shap_values, x_test_ac)

NameError: name 'explainer' is not defined
In [74]:
shap.dependence_plot("BonusMalus", shap_values, x_test_ac)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-74-222672f2cb66> in <module>
----> 1 shap.dependence_plot("BonusMalus", shap_values, x_test_ac)

NameError: name 'shap_values' is not defined
In [75]:
shap.summary_plot(shap_values, x_test_ac)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-75-516659dfff11> in <module>
----> 1 shap.summary_plot(shap_values, x_test_ac)

NameError: name 'shap_values' is not defined
In [76]:
shap.summary_plot(shap_values, x_test_ac, plot_type="bar")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-76-c25af7b0fe00> in <module>
----> 1 shap.summary_plot(shap_values, x_test_ac, plot_type="bar")

NameError: name 'shap_values' is not defined

Использование моделей градиентного бустинга

In [77]:
predictions = pd.DataFrame()
predictions['CountPredicted'] = xgb_freq.predict(xgb.DMatrix(df_freq[col_features]))
predictions['AvgClaimPredicted'] = xgb_avgclaim.predict(xgb.DMatrix(df_freq[col_features]))
In [78]:
predictions['CountPredicted'].min()
Out[78]:
0.05940763279795647
In [79]:
predictions['BurningCost'] = predictions.CountPredicted * predictions.AvgClaimPredicted
predictions.head()
Out[79]:
CountPredicted AvgClaimPredicted BurningCost
0 0.248379 1070.127441 265.797028
1 0.248379 1070.127441 265.797028
2 0.223219 798.282227 178.191437
3 0.218085 895.712341 195.341827
4 0.156707 1120.760132 175.631042

Об особенностях сохранения моделей:

In [85]:
xgb_avgclaim.save_model('D:\Cloud\Git\geekbrains-ml-business\\avg_claim.model')
In [86]:
xgb_avgclaim.feature_names
Out[86]:
['driver_minexp', 'Gender', 'MariStat', 'HasKmLimit', 'BonusMalus', 'OutUseNb', 'RiskArea', 'driver_minage_m', 'driver_minage_f', 'driver_minage_m_2', 'driver_minage_f_2', 'VehUsage_Private', 'VehUsage_Private+trip to office', 'VehUsage_Professional', 'VehUsage_Professional run', 'SocioCateg_CSP1', 'SocioCateg_CSP2', 'SocioCateg_CSP3', 'SocioCateg_CSP4', 'SocioCateg_CSP5', 'SocioCateg_CSP6', 'SocioCateg_CSP7']
In [87]:
xgb_avgclaim.feature_types
Out[87]:
['int', 'int', 'int', 'int', 'int', 'float', 'float', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int']
In [88]:
model =xgb.Booster()
model.load_model('D:\Cloud\Git\geekbrains-ml-business\\avg_claim.model')
In [89]:
print(model.feature_names)
type(model.feature_names)
None
Out[89]:
NoneType
In [90]:
model.feature_types
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-90-fabf70d27454> in <module>
----> 1 model.feature_types

AttributeError: 'Booster' object has no attribute 'feature_types'

* Домашнее задание: Многоклассовая классификация

Задание из Урока:

  • Построить модель градиентного бустинга для показателя частоты страховых убытков.
  • Можно предобработать исходные данные, добавив дополнительные фичи.
  • Также можно использовать различные методы для подбора гиперпараметров.
  • Оценить результаты построенного классификатора, выявить возможные проблемы.
  • Сравнить результаты с полученным ранее результатом с использованием Пуассоновской регрессии.
  • Проанализировать результаты, предложить способы решения обнаруженных проблем и/или попробовать его улучшить.
  • При желании можно использовать любой другой пакет для построения моделей градиентого бустинга.

В текущем домашнем задание предлагается взглянуть на задачу моделирования количества страховых случаев как на задачу многоклассовой классификации.

In [125]:
#pip install "git+https://github.com/MindSetLib/Insuranse_Lesson.git#egg=insolver"
In [157]:
import pandas as pd
import numpy as np
import xgboost as xgb
import matplotlib.pyplot as plt

from sklearn.metrics import f1_score, accuracy_score, confusion_matrix
#from insolver import InsDataframe, InsolverGradientBoostingWrapper, train_val_test_split

import warnings
from functools import partial

from xgboost import DMatrix, cv as xcv, train as xtrain, XGBClassifier, XGBRegressor
#from lightgbm import Dataset, cv as lcv, train as ltrain, LGBMClassifier, LGBMRegressor
#from catboost import Pool, cv as ccv, train as ctrain, CatBoostClassifier, CatBoostRegressor
from hyperopt import hp, tpe, space_eval, Trials, STATUS_OK, STATUS_FAIL, fmin
from sklearn.model_selection import train_test_split
In [154]:
#взято отсюда https://github.com/MindSetLib/Insuranse_Lesson/blob/master/insolver/core.py
def train_val_test_split(x, y, val_size, test_size, random_state=0, shuffle=True, stratify=None):
    x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=random_state, shuffle=shuffle,
                                                        test_size=test_size, stratify=stratify)
    x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train, random_state=random_state, shuffle=shuffle,
                                                          test_size=val_size/(1-test_size), stratify=stratify)
    return x_train, x_valid, x_test, y_train, y_valid, y_test

def objective_gb(params, algorithm, cv_params, data_params, X, y):
    if data_params is None:
        data_params = dict()
    for x in ['max_depth', 'num_boost_round', 'max_leaves', 'max_bin', 'num_leaves',
              'min_child_samples', 'min_data_in_leaf']:
        if x in params.keys():
            params[x] = int(params[x])
    if algorithm == 'xgboost':
        dtrain = DMatrix(X, y, **data_params)
        cv_result = xcv(params=params, dtrain=dtrain, **cv_params)
        name = [i for i in cv_result.columns if all([i.startswith('test-'), i.endswith('-mean')])][-1]
        score = cv_result[name][-1:].values[0]
    elif algorithm == 'lightgbm':
        dtrain = Dataset(X, y, **data_params)
        cv_result = lcv(params=params, train_set=dtrain, **cv_params)
        name = [i for i in cv_result.keys() if i.endswith('-mean')][-1]
        score = cv_result[name][-1]
    elif algorithm == 'catboost':
        dtrain = Pool(X, y, **data_params)
        cv_result = ccv(params=params, dtrain=dtrain, **cv_params)
        name = [i for i in cv_result.columns if all([i.startswith('test-'), i.endswith('-mean')])][-1]
        score = cv_result[name][-1:].values[0]
    else:
        warnings.warn('Error occurred in "algorithm" attribute')
        score = 0
    return {'loss': score, 'status': STATUS_OK}
In [128]:
#взято отсюда: https://github.com/MindSetLib/Insuranse_Lesson/blob/master/insolver/core.py
class InsolverGradientBoostingWrapper(object):
    def __init__(self, algorithm, task='classification'):
        if algorithm in ['xgboost', 'lightgbm', 'catboost']:
            self.algorithm = algorithm
        else:
            warnings.warn('Specified algorithm parameter is not supported. '
                          'Try to enter one of the following options: ["xgboost", "lightgbm", "catboost"].')
        if task in ['regression', 'classification']:
            self.task = task
        else:
            warnings.warn('Specified task parameter is not supported. '
                          'Try to enter one of the following options: ["regression", "classification"].')
        self.trials, self.best_params, self.core_params, self.data_params = None, None, None, None
        self.model, self.booster = None, None

    @staticmethod
    def cv_parameters_default_xgboost():
        return {'num_boost_round': 10,
                'nfold': 3,
                'stratified': False,
                'folds': None,
                'metrics': (),
                'obj': None,
                'feval': None,
                'maximize': False,
                'early_stopping_rounds': None,
                'fpreproc': None,
                'as_pandas': True,
                'verbose_eval': None,
                'show_stdv': True,
                'seed': 0,
                'callbacks': None,
                'shuffle': True}

    @staticmethod
    def cv_parameters_default_lightgbm():
        return {'num_boost_round': 100,
                'folds': None,
                'nfold': 5,
                'stratified': True,
                'shuffle': True,
                'metrics': None,
                'fobj': None,
                'feval': None,
                'init_model': None,
                'feature_name': 'auto',
                'categorical_feature': 'auto',
                'early_stopping_rounds': None,
                'fpreproc': None,
                'verbose_eval': None,
                'show_stdv': True,
                'seed': 0,
                'callbacks': None,
                'eval_train_metric': False,
                'return_cvbooster': False}

    @staticmethod
    def cv_parameters_default_catboost():
        return {'iterations': None,
                'num_boost_round': None,
                'fold_count': 3,
                'nfold': None,
                'inverted': False,
                'partition_random_seed': 0,
                'seed': None,
                'shuffle': True,
                'logging_level': None,
                'stratified': None,
                'as_pandas': True,
                'metric_period': None,
                'verbose': None,
                'verbose_eval': None,
                'plot': False,
                'early_stopping_rounds': None,
                'folds': None,
                'type': 'Classical'}

    def hyperopt_cv(self, X, y, params, cv_params, data_params=None, max_evals=10, fn=None, algo=None, timeout=None):
        trials = Trials()
        if data_params is not None:
            self.data_params = data_params
        if algo is None:
            algo = tpe.suggest
        if fn is None:
            fn = partial(objective_gb, algorithm=self.algorithm, X=X, y=y, cv_params=cv_params, data_params=data_params)
        try:
            best = fmin(fn=fn, space=params, trials=trials, algo=algo, max_evals=max_evals, timeout=timeout)
            best_params = space_eval(params, best)
            for x in ['max_depth', 'num_boost_round', 'max_leaves', 'max_bin', 'num_leaves',
                      'min_child_samples', 'min_data_in_leaf']:
                if x in best_params.keys():
                    best_params[x] = int(best_params[x])
            self.best_params, self.trials = best_params, trials

            core_params = ['num_boost_round', 'obj', 'feval', 'maximize', 'early_stopping_rounds',
                           'verbose_eval', 'callbacks', 'fobj', 'init_model', 'feature_name', 'categorical_feature',
                           'iterations', 'logging_level', 'metric_period', 'verbose', 'plot']
            self.core_params = {i: cv_params[i] for i in cv_params if i in core_params}

        except Exception as e:
            return {'status': STATUS_FAIL, 'exception': str(e)}

    def fit_booster(self, X, y, data_params=None, core_params=None):
        dtrain_params, train_params = dict(), dict()
        if self.data_params is not None:
            dtrain_params.update(self.data_params)
        if data_params is not None:
            dtrain_params.update(data_params)
        if self.core_params is not None:
            train_params.update(self.core_params)
        if core_params is not None:
            train_params.update(core_params)
        if self.best_params is not None:
            params = self.best_params
        else:
            params = {}

        if self.algorithm == 'xgboost':
            dtrain = DMatrix(X, y, **dtrain_params)
            if 'evals' in train_params.keys():
                train_params['evals'] = [(DMatrix(i[0][0], i[0][1]), i[1]) for i in train_params['evals']]
            self.booster = xtrain(params=params, dtrain=dtrain, **train_params)
        elif self.algorithm == 'lightgbm':
            dtrain = Dataset(X, y, **dtrain_params)
            if 'evals' in train_params.keys():
                evals = train_params.pop('evals')
                train_params['valid_names'] = [i[1] for i in evals]
                train_params['valid_sets'] = [(Dataset(i[0][0], i[0][1])) for i in evals]
            self.booster = ltrain(params=params, train_set=dtrain, **train_params)
        elif self.algorithm == 'catboost':
            dtrain = Pool(X, y, **dtrain_params)
            if 'evals' in train_params.keys():
                train_params['evals'] = [Pool(i[0][0], i[0][1]) for i in train_params['evals']]
            self.booster = ctrain(params=params, dtrain=dtrain, **train_params)
        else:
            warnings.warn('Specified algorithm parameter is not supported.')

    def model_init(self, params=None):
        hparams = dict()
        if self.core_params is not None:
            hparams.update(self.core_params)
        if self.best_params is not None:
            hparams.update(self.best_params)
        if params is not None:
            hparams.update(params)

        aliases = {'eta': 'learning_rate', 'boosting': 'boosting_type', 'max_leaves': 'num_leaves',
                   'num_iterations': 'n_estimators', 'num_iteration': 'n_estimators',
                   'n_iter': 'n_estimators', 'num_tree': 'n_estimators', 'num_trees': 'n_estimators',
                   'num_round': 'n_estimators', 'num_rounds': 'n_estimators', 'num_boost_round': 'n_estimators'}

        for x in hparams.keys():
            if x in aliases.keys():
                hparams[aliases[x]] = hparams.pop(x)

        if self.task == 'classification':
            if self.algorithm == 'xgboost':
                self.model = XGBClassifier(**hparams)
            elif self.algorithm == 'lightgbm':
                self.model = LGBMClassifier(**hparams)
            elif self.algorithm == 'catboost':
                self.model = CatBoostClassifier(**hparams)
            else:
                warnings.warn('Specified algorithm parameter is not supported.')
        elif self.task == 'regression':
            if self.algorithm == 'xgboost':
                self.model = XGBRegressor(**hparams)
            elif self.algorithm == 'lightgbm':
                self.model = LGBMRegressor(**hparams)
            elif self.algorithm == 'catboost':
                self.model = CatBoostRegressor(**hparams)
            else:
                warnings.warn('Specified algorithm parameter is not supported.')
        else:
            warnings.warn('Tasks other than "classification" and "regression" are not supported.')

    def fit(self, X, y, **kwargs):
        if self.model is None:
            warnings.warn('Model is not initiated, please use .model_init() method.')
        else:
            self.model.fit(X, y, **kwargs)
In [129]:
#df = pd.read_csv('/content/drive/My Drive/Colab Notebooks/freMPL-R.csv', low_memory=False)
df = pd.read_csv('D:\Cloud\Git\geekbrains-ml-business\\freMPL-R.csv', low_memory=False)
In [130]:
df = df.loc[df.Dataset.isin([5, 6, 7, 8, 9])]
df.drop('Dataset', axis=1, inplace=True)
df.dropna(axis=1, how='all', inplace=True)
df.drop_duplicates(inplace=True)
df.reset_index(drop=True, inplace=True)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 20 columns):
 #   Column             Non-Null Count   Dtype  
---  ------             --------------   -----  
 0   Exposure           115155 non-null  float64
 1   LicAge             115155 non-null  int64  
 2   RecordBeg          115155 non-null  object 
 3   RecordEnd          59455 non-null   object 
 4   Gender             115155 non-null  object 
 5   MariStat           115155 non-null  object 
 6   SocioCateg         115155 non-null  object 
 7   VehUsage           115155 non-null  object 
 8   DrivAge            115155 non-null  int64  
 9   HasKmLimit         115155 non-null  int64  
 10  BonusMalus         115155 non-null  int64  
 11  ClaimAmount        115155 non-null  float64
 12  ClaimInd           115155 non-null  int64  
 13  ClaimNbResp        115155 non-null  float64
 14  ClaimNbNonResp     115155 non-null  float64
 15  ClaimNbParking     115155 non-null  float64
 16  ClaimNbFireTheft   115155 non-null  float64
 17  ClaimNbWindscreen  115155 non-null  float64
 18  OutUseNb           115155 non-null  float64
 19  RiskArea           115155 non-null  float64
dtypes: float64(9), int64(5), object(6)
memory usage: 17.6+ MB

Предобработайте данные

В предыдущем уроке мы заметили отрицательную величину убытка для некоторых наблюдений. Заметим, что для всех таких полисов переменная "ClaimInd" принимает только значение 0. Поэтому заменим все соответствующие значения "ClaimAmount" нулями.

In [131]:
NegClaimAmount = df.loc[df.ClaimAmount < 0, ['ClaimAmount','ClaimInd']]
print('Unique values of ClaimInd:', NegClaimAmount.ClaimInd.unique())
Unique values of ClaimInd: [0]
In [132]:
df.loc[df.ClaimAmount < 0, 'ClaimAmount'] = 0
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 20 columns):
 #   Column             Non-Null Count   Dtype  
---  ------             --------------   -----  
 0   Exposure           115155 non-null  float64
 1   LicAge             115155 non-null  int64  
 2   RecordBeg          115155 non-null  object 
 3   RecordEnd          59455 non-null   object 
 4   Gender             115155 non-null  object 
 5   MariStat           115155 non-null  object 
 6   SocioCateg         115155 non-null  object 
 7   VehUsage           115155 non-null  object 
 8   DrivAge            115155 non-null  int64  
 9   HasKmLimit         115155 non-null  int64  
 10  BonusMalus         115155 non-null  int64  
 11  ClaimAmount        115155 non-null  float64
 12  ClaimInd           115155 non-null  int64  
 13  ClaimNbResp        115155 non-null  float64
 14  ClaimNbNonResp     115155 non-null  float64
 15  ClaimNbParking     115155 non-null  float64
 16  ClaimNbFireTheft   115155 non-null  float64
 17  ClaimNbWindscreen  115155 non-null  float64
 18  OutUseNb           115155 non-null  float64
 19  RiskArea           115155 non-null  float64
dtypes: float64(9), int64(5), object(6)
memory usage: 17.6+ MB
In [133]:
df.head()
Out[133]:
Exposure LicAge RecordBeg RecordEnd Gender MariStat SocioCateg VehUsage DrivAge HasKmLimit BonusMalus ClaimAmount ClaimInd ClaimNbResp ClaimNbNonResp ClaimNbParking ClaimNbFireTheft ClaimNbWindscreen OutUseNb RiskArea
0 0.083 332 2004-01-01 2004-02-01 Male Other CSP50 Professional 46 0 50 0.0 0 0.0 1.0 0.0 0.0 0.0 0.0 9.0
1 0.916 333 2004-02-01 NaN Male Other CSP50 Professional 46 0 50 0.0 0 0.0 1.0 0.0 0.0 0.0 0.0 9.0
2 0.550 173 2004-05-15 2004-12-03 Male Other CSP50 Private+trip to office 32 0 68 0.0 0 0.0 2.0 0.0 0.0 0.0 0.0 7.0
3 0.089 364 2004-11-29 NaN Female Other CSP55 Private+trip to office 52 0 50 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 8.0
4 0.233 426 2004-02-07 2004-05-01 Male Other CSP60 Private 57 0 50 0.0 0 0.0 0.0 0.0 0.0 0.0 0.0 7.0

Для моделирования частоты убытков сгенерируем показатель как сумму индикатора того, что убыток произошел ("ClaimInd") и количества заявленных убытков по различным видам ущерба за 4 предшествующих года ("ClaimNbResp", "ClaimNbNonResp", "ClaimNbParking", "ClaimNbFireTheft", "ClaimNbWindscreen").

В случаях, если соответствующая величина убытка равняется нулю, сгенерированную частоту также обнулим.

In [134]:
df['ClaimsCount'] = df.ClaimInd + df.ClaimNbResp + df.ClaimNbNonResp + df.ClaimNbParking + df.ClaimNbFireTheft + df.ClaimNbWindscreen
df.loc[df.ClaimAmount == 0, 'ClaimsCount'] = 0
df.drop(["ClaimNbResp", "ClaimNbNonResp", "ClaimNbParking", "ClaimNbFireTheft", "ClaimNbWindscreen"], axis=1, inplace=True)
In [135]:
pd.DataFrame(df.ClaimsCount.value_counts()).rename({'ClaimsCount': 'Policies'}, axis=1)
Out[135]:
Policies
0.0 104286
2.0 3529
1.0 3339
3.0 2310
4.0 1101
5.0 428
6.0 127
7.0 26
8.0 6
9.0 2
11.0 1
In [136]:
data = InsDataFrame_Fr()
data.load_pd(df)
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 16 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   Exposure     115155 non-null  float64
 1   LicAge       115155 non-null  int64  
 2   RecordBeg    115155 non-null  object 
 3   RecordEnd    59455 non-null   object 
 4   Gender       115155 non-null  object 
 5   MariStat     115155 non-null  object 
 6   SocioCateg   115155 non-null  object 
 7   VehUsage     115155 non-null  object 
 8   DrivAge      115155 non-null  int64  
 9   HasKmLimit   115155 non-null  int64  
 10  BonusMalus   115155 non-null  int64  
 11  ClaimAmount  115155 non-null  float64
 12  ClaimInd     115155 non-null  int64  
 13  OutUseNb     115155 non-null  float64
 14  RiskArea     115155 non-null  float64
 15  ClaimsCount  115155 non-null  float64
dtypes: float64(5), int64(5), object(6)
memory usage: 14.1+ MB
In [137]:
# Переименовываем
data.columns_match({'DrivAge':'driver_minage','LicAge':'driver_minexp'})
# Преобразовываем
data.transform_age()
data.transform_exp()
data.transform_gender()
data.transform_MariStat()
data.transform_SocioCateg()
# Пересечение пола и возраста, их квадраты
data.transform_age_gender()
data.polynomizer('driver_minage_m')
data.polynomizer('driver_minage_f')
In [138]:
# Onehot encoding Для переменных, содержащих более 2 значений:
data.get_dummies(['VehUsage','SocioCateg'])
In [139]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 29 columns):
 #   Column                           Non-Null Count   Dtype  
---  ------                           --------------   -----  
 0   Exposure                         115155 non-null  float64
 1   driver_minexp                    115155 non-null  int64  
 2   RecordBeg                        115155 non-null  object 
 3   RecordEnd                        59455 non-null   object 
 4   Gender                           115155 non-null  int64  
 5   MariStat                         115155 non-null  int64  
 6   driver_minage                    115155 non-null  int64  
 7   HasKmLimit                       115155 non-null  int64  
 8   BonusMalus                       115155 non-null  int64  
 9   ClaimAmount                      115155 non-null  float64
 10  ClaimInd                         115155 non-null  int64  
 11  OutUseNb                         115155 non-null  float64
 12  RiskArea                         115155 non-null  float64
 13  ClaimsCount                      115155 non-null  float64
 14  driver_minage_m                  115155 non-null  int64  
 15  driver_minage_f                  115155 non-null  int64  
 16  driver_minage_m_2                115155 non-null  int64  
 17  driver_minage_f_2                115155 non-null  int64  
 18  VehUsage_Private                 115155 non-null  uint8  
 19  VehUsage_Private+trip to office  115155 non-null  uint8  
 20  VehUsage_Professional            115155 non-null  uint8  
 21  VehUsage_Professional run        115155 non-null  uint8  
 22  SocioCateg_CSP1                  115155 non-null  uint8  
 23  SocioCateg_CSP2                  115155 non-null  uint8  
 24  SocioCateg_CSP3                  115155 non-null  uint8  
 25  SocioCateg_CSP4                  115155 non-null  uint8  
 26  SocioCateg_CSP5                  115155 non-null  uint8  
 27  SocioCateg_CSP6                  115155 non-null  uint8  
 28  SocioCateg_CSP7                  115155 non-null  uint8  
dtypes: float64(5), int64(11), object(2), uint8(11)
memory usage: 17.0+ MB
In [140]:
col_features = [
                'driver_minexp',
                'Gender',
                'MariStat',
                'HasKmLimit',
                'BonusMalus',
                'OutUseNb',
                'RiskArea',
                'driver_minage_m',
                'driver_minage_f',
                'driver_minage_m_2',
                'driver_minage_f_2',
                'VehUsage_Private',
                'VehUsage_Private+trip to office',
                'VehUsage_Professional',
                'VehUsage_Professional run',
                'SocioCateg_CSP1',
                'SocioCateg_CSP2',
                'SocioCateg_CSP3',
                'SocioCateg_CSP4',
                'SocioCateg_CSP5',
                'SocioCateg_CSP6',
                'SocioCateg_CSP7'  
]
#col_target = ['ClaimAmount', 'ClaimsCount', 'AvgClaim']
col_target = ['ClaimAmount','ClaimsCount']
In [141]:
df_freq = data.get_pd(col_features+col_target)
df_freq.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 115155 entries, 0 to 115154
Data columns (total 24 columns):
 #   Column                           Non-Null Count   Dtype  
---  ------                           --------------   -----  
 0   driver_minexp                    115155 non-null  int64  
 1   Gender                           115155 non-null  int64  
 2   MariStat                         115155 non-null  int64  
 3   HasKmLimit                       115155 non-null  int64  
 4   BonusMalus                       115155 non-null  int64  
 5   OutUseNb                         115155 non-null  float64
 6   RiskArea                         115155 non-null  float64
 7   driver_minage_m                  115155 non-null  int64  
 8   driver_minage_f                  115155 non-null  int64  
 9   driver_minage_m_2                115155 non-null  int64  
 10  driver_minage_f_2                115155 non-null  int64  
 11  VehUsage_Private                 115155 non-null  uint8  
 12  VehUsage_Private+trip to office  115155 non-null  uint8  
 13  VehUsage_Professional            115155 non-null  uint8  
 14  VehUsage_Professional run        115155 non-null  uint8  
 15  SocioCateg_CSP1                  115155 non-null  uint8  
 16  SocioCateg_CSP2                  115155 non-null  uint8  
 17  SocioCateg_CSP3                  115155 non-null  uint8  
 18  SocioCateg_CSP4                  115155 non-null  uint8  
 19  SocioCateg_CSP5                  115155 non-null  uint8  
 20  SocioCateg_CSP6                  115155 non-null  uint8  
 21  SocioCateg_CSP7                  115155 non-null  uint8  
 22  ClaimAmount                      115155 non-null  float64
 23  ClaimsCount                      115155 non-null  float64
dtypes: float64(4), int64(9), uint8(11)
memory usage: 12.6 MB

XGBoost для многоклассовой классификации принимает на вход значения меток классов в виде [0, num_classes]. Заменим значение 11 на 10.

In [142]:
df_freq['ClaimsCount'] = df_freq['ClaimsCount'].replace({11: 10}).fillna(0)
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

Посмотрим, сколько полисов соответствуют каждому из значений ClaimsCount, используя метод groupby. Для полученных значений также посчитаем нормированную частоту.

In [143]:
FreqCount = pd.DataFrame(df_freq.groupby('ClaimsCount').size(), columns=['Count'])
#FreqCount['Freq'] = '<Ваш код здесь>'
In [144]:
FreqCount.Count.plot(kind='bar')
plt.ylabel('Frequency')
plt.show()
In [145]:
FreqCount
Out[145]:
Count
ClaimsCount
0.0 104286
1.0 3339
2.0 3529
3.0 2310
4.0 1101
5.0 428
6.0 127
7.0 26
8.0 6
9.0 2
10.0 1

Заметим, что в данном случае присутствует проблема несбалансированности классов. Поэтому, для того, чтобы по возможности избежать ее, воспользуемся взвешиванием наблюдений для обучения модели. Для этого в исходном наборе данных создадим столбец weight. Присвоим ему некоторые значения, например, можно задать 0.05 для значений ClaimsCount 0, а для остальных - 1 (Для этого можем использовать функцию np.where). Также можно попробовать какой-либо другой способ задания весов, приведенный пример не гарантирует хороших результатов.

In [146]:
#поставим свое значение
df_freq['weight'] = np.where(df_freq['ClaimsCount'] == 0, 0.15, 1)
df_freq
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
Out[146]:
driver_minexp Gender MariStat HasKmLimit BonusMalus OutUseNb RiskArea driver_minage_m driver_minage_f driver_minage_m_2 ... SocioCateg_CSP1 SocioCateg_CSP2 SocioCateg_CSP3 SocioCateg_CSP4 SocioCateg_CSP5 SocioCateg_CSP6 SocioCateg_CSP7 ClaimAmount ClaimsCount weight
0 52 0 0 0 50 0.0 9.0 46 18 2116 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
1 52 0 0 0 50 0.0 9.0 46 18 2116 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
2 52 0 0 0 68 0.0 7.0 32 18 1024 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
3 52 1 0 0 50 0.0 8.0 18 52 324 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
4 52 0 0 0 50 0.0 7.0 57 18 3249 ... 0 0 0 0 0 1 0 0.000000 0.0 0.15
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
115150 52 0 0 0 50 4.0 8.0 39 18 1521 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
115151 52 1 0 0 50 0.0 7.0 18 54 324 ... 0 0 0 0 1 0 0 2764.169184 2.0 1.00
115152 52 0 0 0 54 0.0 7.0 35 18 1225 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
115153 52 0 0 0 50 0.0 7.0 52 18 2704 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15
115154 52 0 0 0 50 0.0 7.0 52 18 2704 ... 0 0 0 0 1 0 0 0.000000 0.0 0.15

115155 rows × 25 columns

Разобьем имеющийся набор данных на обучающую, валидационную и тестовую выборки в отношениях 70%/15%/15% соответственно. Зададим зерно для случайного разбиения равным 10.

In [148]:
x_train, x_valid, x_test, \
y_train, y_valid, y_test = train_val_test_split(df_freq.drop(['ClaimAmount','ClaimsCount'], axis = 1)
                                               ,df_freq.ClaimsCount,val_size=0.15, test_size=0.15, random_state=10)

Далее, создадим объекты DMatrix для обучающей, валидационной и тестовой выборок. Для обучающей выборки также укажем параметр weight равным полученному ранее столбцу весов. Данный столбец также нужно исключить из объекта передаваемого в параметр data.

In [149]:
igb = InsolverGradientBoostingWrapper(algorithm='xgboost')
In [150]:
space_xgboost = {'objective': 'multi:softmax',
                'max_depth': hp.choice('max_depth', [5, 8, 10, 12, 15]),
                'min_child_weight': hp.uniform('min_child_weight', 0, 50),
                'subsample': hp.uniform('subsample', 0.5, 1),
                'colsample_bytree': hp.uniform('colsample_bytree', 0.5, 1),
                'alpha': hp.uniform('alpha', 0, 1),
                'lambda': hp.uniform('lambda', 0, 1),
                'eta': hp.uniform('eta', 0.01, 1),
                'gamma': hp.uniform('gamma', 0.01, 1000),
                'num_class': len(df_freq.ClaimsCount.unique()),#тут число классов
                'tree_method': 'hist'
              }
In [151]:
cv_params = {'num_boost_round': 1000,
             'nfold': 3,
             'early_stopping_rounds': 20,
             'seed': 0,
             'shuffle': True,
             'stratified': False }
In [158]:
igb.hyperopt_cv(x_train.drop('weight', axis=1), y_train, space_xgboost, cv_params,
               data_params={'weight': x_train['weight']}, max_evals=50)
100%|████████████████████████████████████████████████| 50/50 [19:23<00:00, 23.28s/trial, best loss: 0.4115146666666667]

Для оптимизации гиперпараметров можно воспользоваться различными методами.

Далее обучим нашу модель с оптимальными параметрами

In [159]:
igb.fit_booster(x_train.drop('weight', axis=1), y_train, core_params={'evals': [((x_train.drop('weight', axis=1),y_train),'train'),
                                                                                ((x_valid.drop('weight', axis=1),y_valid),'valid')]})
[0]	train-merror:0.09494	valid-merror:0.09262
Multiple eval metrics have been passed: 'valid-merror' will be used for early stopping.

Will train until valid-merror hasn't improved in 20 rounds.
[1]	train-merror:0.09494	valid-merror:0.09262
[2]	train-merror:0.09494	valid-merror:0.09262
[3]	train-merror:0.09494	valid-merror:0.09262
[4]	train-merror:0.09494	valid-merror:0.09262
[5]	train-merror:0.09493	valid-merror:0.09262
[6]	train-merror:0.09507	valid-merror:0.09286
[7]	train-merror:0.09527	valid-merror:0.09320
[8]	train-merror:0.09531	valid-merror:0.09326
[9]	train-merror:0.09524	valid-merror:0.09320
[10]	train-merror:0.09516	valid-merror:0.09315
[11]	train-merror:0.09518	valid-merror:0.09315
[12]	train-merror:0.09529	valid-merror:0.09338
[13]	train-merror:0.09543	valid-merror:0.09355
[14]	train-merror:0.09548	valid-merror:0.09378
[15]	train-merror:0.09551	valid-merror:0.09355
[16]	train-merror:0.09541	valid-merror:0.09320
[17]	train-merror:0.09546	valid-merror:0.09361
[18]	train-merror:0.09554	valid-merror:0.09373
[19]	train-merror:0.09565	valid-merror:0.09407
[20]	train-merror:0.09580	valid-merror:0.09436
Stopping. Best iteration:
[0]	train-merror:0.09494	valid-merror:0.09262

Посчитаем метрики accuracy и f1 на наших наборах данных, также можем визуализировать confusion matrix, например, с помощью plt.imshow(). Можно использовать предложенный ниже код.

In [164]:
dfsets = [{'set': 'train', 'x': x_train.drop('weight', axis=1), 'target': y_train},
          {'set': 'valid', 'x': x_valid.drop('weight', axis=1), 'target': y_valid},
          {'set': 'test', 'x': x_test.drop('weight', axis=1), 'target': y_test}]
for dfset in dfsets:
    class_preds = igb.booster.predict(xgb.DMatrix(dfset['x'])) # Посчитаем предсказанные значения
    print('F1 Score on ' + str(dfset['set'])+':', f1_score(dfset['target'],class_preds, average='micro')) # Посчитаем F1 Score
F1 Score on train: 0.904201868324091
F1 Score on valid: 0.9056385318976496
F1 Score on test: 0.9046543938867663
In [169]:
plt.subplots(1,3, figsize=(15,3))
for i in range(len(dfsets)):
    confmatrix = confusion_matrix(dfsets[i]['target'], igb.booster.predict(xgb.DMatrix(dfsets[i]['x'])))
    plt.subplot(1,3,i+1)
    plt.imshow(confmatrix, cmap='Greys')
    plt.colorbar()
    plt.ylabel('True')
    plt.xlabel('Predicted')
plt.show()

Как вы оцениваете качество построенной модели? Какие проблемы могут здесь присутствовать? Как можно улучшить результат?

Ответ: изначально наблюдался дисбаланс классов (возобладал класс 0), отсюда такой результат. Подобную задачу следует решать с помощью регрессии, а не классификации.

In [ ]: